package org.xx.armory;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xx.armory.commons.AbstractLifeCycle;
import org.xx.armory.commons.LifeCycle;
import org.xx.armory.config.ConfigurationManager;
import org.xx.armory.config.XmlFileConfigurationProvider;
import org.xx.armory.log4j2.Log4j2Configurator;
import org.xx.armory.mbeans.ApplicationProxy;

import javax.management.JMException;
import javax.management.remote.JMXConnectorServer;
import javax.management.remote.JMXConnectorServerFactory;
import javax.management.remote.JMXServiceURL;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.rmi.registry.LocateRegistry;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.lang.Runtime.getRuntime;
import static java.lang.System.getProperty;
import static java.lang.System.getenv;
import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.commons.lang3.math.NumberUtils.toInt;
import static org.xx.armory.commons.MBeanUtils.registerMBean;
import static org.xx.armory.commons.MBeanUtils.unregisterMBean;
import static org.xx.armory.commons.Validators.notNull;
import static org.xx.armory.mbeans.ApplicationProxy.JMX_DOMAIN;

/**
 * 应用程序对象。
 *
 * @author Haart
 */
public abstract class Application
        extends AbstractLifeCycle
        implements LifeCycle {
    protected static final int LOG_TRACE = 1;
    protected static final int LOG_DEBUG = 2;
    protected static final int LOG_INFO = 3;
    protected static final int LOG_WARN = 4;
    protected static final int LOG_ERROR = 5;
    protected static final int DEFAULT_JXM_RMI_PORT = 9090;
    private volatile String appHome;
    private int jmxrmiPort;

    /**
     * 构造{@code Application}对象实例。
     */
    protected Application() {
        this.appHome = "";
        this.jmxrmiPort = DEFAULT_JXM_RMI_PORT;
    }

    /**
     * 主入口方法。
     *
     * <p>设置必要的全局函数和属性，然后解析命令行参数，最后根据解析结果执行主入口方法。</p>
     *
     * @param app
     *         待执行的应用程序。
     * @param args
     *         命令行参数。
     * @throws IllegalArgumentException
     *         如果参数{@code app}是{@code null}。
     * @throws javax.management.JMException
     *         如果注册应用程序为MXBean时出现错误。
     * @throws Exception
     *         如果应用程序启动时出现错误。
     */
    public static void main(
            Application app,
            String[] args
    )
            throws Exception {
        notNull(app, "app");

        getRuntime().addShutdownHook(new Thread(() -> {
            try {
                // 终止应用程序并释放所有资源。
                app.shutdown();
                app.close();
            } catch (Exception ignored) {
            }
        }));

        final List<String> argList;
        if (args != null) {
            argList = new ArrayList<>(args.length);
            argList.addAll(asList(args));
        } else {
            argList = new ArrayList<>();
        }

        if (app.parseArguments(argList)) {
            try {
                app.initialize();
            } catch (Exception ex) {
                app.log(LOG_ERROR, "cannot initialize application", ex);
                return;
            }

            app.executeMain();
        }
    }

    /**
     * 获取应用程序的名字。
     *
     * @return 应用程序的名字。
     */
    public String getName() {
        return "app";
    }

    /**
     * 获取应用程序的描述。
     *
     * @return 应用程序的描述。
     */
    public String getDescription() {
        return "Application";
    }

    /**
     * 获取应用程序的当前状态描述。
     *
     * @return 应用程序的当前状态描述。
     */
    public String getStatus() {
        return "";
    }

    /**
     * 启动应用程序。
     *
     * @throws Exception
     *         如果启动过程中出现错误。
     */
    public void start()
            throws Exception {
    }

    /**
     * 终止应用程序。
     *
     * @throws Exception
     *         如果终止过程中出现错误。
     */
    public void shutdown()
            throws Exception {
    }

    /**
     * 获取JMX-RMI注册端口号。
     *
     * @return JMX-RMI注册端口号。
     */
    public final synchronized int getJMXRMIPort() {
        return this.jmxrmiPort;
    }

    /**
     * 设置JMX-RMI注册端口号。
     *
     * @param jmxPort
     *         JMX-RMI注册端口号。
     */
    public final synchronized void setJMXRMIPort(
            int jmxPort
    ) {
        this.jmxrmiPort = jmxPort;
    }

    /**
     * 解析命令行参数。
     *
     * @param args
     *         待解析的命令行参数。
     * @return 是否解析成功。
     */
    protected boolean parseArguments(
            List<String> args
    ) {
        final Options opts = new Options()
                .addOption("", "jmxrmi-port", true, "port of JMX RMI");

        try {
            final CommandLine commandLine = new DefaultParser().parse(opts, args.toArray(new String[0]), null, true);

            final int jmxrmiPort = toInt(commandLine.getOptionValue("jmxrmi-port", ""));

            setJMXRMIPort(jmxrmiPort);

            // 清空原有的参数，只保留未解析成功的参数。
            args.clear();
            args.addAll(commandLine.getArgList());

            return true;
        } catch (ParseException | IllegalArgumentException ex) {
            new HelpFormatter().printHelp(getName() + " [options] ...", "", opts, "", true);
            return false;
        }
    }

    /**
     * {@inheritDoc}
     * <ol>
     * <li>注册配置提供程序。</li>
     * <li>调用{@link #initialize()}方法初始化应用程序。</li>
     * </ol>
     */
    @Override
    public void initialize()
            throws Exception {
        super.initialize();

        // 注册基于XML文件的配置提供程序。
        ConfigurationManager.register(new XmlFileConfigurationProvider(getConfigurationUrl("armory.xml")));

        // 初始化log4j2日志系统。
        Log4j2Configurator.newInstance(ConfigurationManager.getConfiguration()).configure();

        registerMBean(new ApplicationProxy(this), JMX_DOMAIN, this.getClass().getSimpleName(), "");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void dispose()
            throws Exception {
        unregisterMBean(JMX_DOMAIN, this.getClass().getSimpleName(), "");
    }

    /**
     * 从配置文件夹读取文件。
     *
     * <p>
     * 如果配置文件的路径是相对路径，那么位于{@literal conf}文件夹下。
     * </p>
     *
     * @param name
     *         相对于配置文件夹的路径，可以是绝对路径，如果此参数是{@code null}或者只包含空白字符，那么返回配置文件夹本身。
     * @return 文件路径，如果参数{@code name}是绝对路径那么返回{@code name}，否则返回相对于{@literal AppHome}的路径。
     */
    protected final String getConfigurationUrl(
            String name
    ) {
        name = trimToEmpty(name);
        final Path rootPath = Paths.get(getAppHome()).resolve("conf");
        if (!name.isEmpty()) {
            return rootPath.resolve(name).normalize().toString();
        } else {
            return rootPath.normalize().toString();
        }
    }

    /**
     * 获取应用程序的根路径。
     *
     * <p>根路径来自于环境变量{@literal APP_HOME}，如果此环境变量不存在则使用当前目录。</p>
     *
     * @return 应用程序的根路径。
     */
    private String getAppHome() {
        String appHome_ = this.appHome;

        if (appHome_.isEmpty()) {
            synchronized (this) {
                appHome_ = this.appHome;
                if (appHome_.isEmpty()) {
                    // 通过环境变量获取APP_HOME
                    appHome_ = trimToEmpty(getenv("APP_HOME"));
                    if (appHome_.isEmpty()) {
                        // 如果未指定环境变量则使用当前路径作为APP_HOME。
                        appHome_ = getProperty("user.dir");
                    }
                }
                this.appHome = trimToEmpty(appHome_);
            }
        }

        return this.appHome;
    }

    /**
     * 执行主入口方法。
     *
     * @throws Exception
     *         如果执行主入口方法时出现错误。
     */
    protected void executeMain()
            throws Exception {
        startJMXServer();
    }

    /**
     * 启动JMX连接服务器，允许客户端检查和控制应用程序的状态。
     *
     * @throws IllegalArgumentException
     *         如果参数{@code application}是{@code null}。
     * @throws IOException
     *         如果启动服务器出错。
     * @throws JMException
     *         如果注册MBean出错。
     */
    private void startJMXServer()
            throws IOException, JMException {
        final int jmxrmiPort = getJMXRMIPort();

        if (jmxrmiPort <= 0) {
            return;
        }

        // JMX服务端口号等于JMX-RMI注册端口号+1.
        final int jmxsrvPort = jmxrmiPort + 1;

        final String host = "0.0.0.0";

        // JMX URL 格式：service:jmx:rmi://[host]:[jmx-server-port]/jndi/rmi://[host]:[rmi-registry-port]/jmxrmi
        LocateRegistry.createRegistry(jmxrmiPort);

        final String jmxsrvAddr = host + ":" + jmxsrvPort;
        final String jmxrmiAddr = host + ":" + jmxrmiPort;
        final JMXServiceURL jmxUrl = new JMXServiceURL("service:jmx:rmi://" + jmxsrvAddr + "/jndi/rmi://" + jmxrmiAddr + "/jmxrmi");

        final Map<String, Object> environment = new HashMap<>();
        environment.put("com.sun.mbeans.jmxremote.authenticate", false);
        environment.put("com.sun.mbeans.jmxremote.ssl", false);

        // TODO: 加入AuthInfo
        final JMXConnectorServer jmxServer = JMXConnectorServerFactory.newJMXConnectorServer(jmxUrl, environment, ManagementFactory.getPlatformMBeanServer());
        jmxServer.start();

        log(LOG_INFO, "jmx server url: " + jmxUrl);
    }

    /**
     * 输出指定级别的日志。
     * <p>如果当前应用程序已初始化完成，那么通过日志系统输出。否则通过控制台输出。</p>
     *
     * @param level
     *         日志级别，必须是 {@link #LOG_TRACE}、{@link #LOG_DEBUG}、{@link #LOG_INFO}、{@link #LOG_WARN}或者{@link #LOG_ERROR}。
     * @param msg
     *         日志消息。
     * @throws IllegalArgumentException
     *         如果参数{@code level}不是合法的日志级别。
     */
    protected final void log(
            int level,
            String msg
    ) {
        log(level, msg, null);
    }

    /**
     * 输出指定级别的日志。
     * <p>如果当前应用程序已初始化完成，那么通过日志系统输出。否则通过控制台输出。</p>
     *
     * @param level
     *         日志级别，必须是 {@link #LOG_TRACE}、{@link #LOG_DEBUG}、{@link #LOG_INFO}、{@link #LOG_WARN}或者{@link #LOG_ERROR}。
     * @param msg
     *         日志消息。
     * @param throwable
     *         导致此日志的异常。
     * @throws IllegalArgumentException
     *         如果参数{@code level}不是合法的日志级别。
     */
    protected final void log(
            int level,
            String msg,
            Throwable throwable
    ) {
        if (!isActive()) {
            // 通过控制台输出日志。
            final Date now = new Date();
            switch (level) {
                case LOG_TRACE:
                    System.out.printf("%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [TRACE] %2$s%n", now, msg);
                    break;
                case LOG_DEBUG:
                    System.out.printf("%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [DEBUG] %2$s%n", now, msg);
                    break;
                case LOG_INFO:
                    System.out.printf("%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [INFO ] %2$s%n", now, msg);
                    break;
                case LOG_WARN:
                    System.err.printf("%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [WARN ] %2$s%n", now, msg);
                    if (throwable != null) {
                        throwable.printStackTrace(System.err);
                    }
                    break;
                case LOG_ERROR:
                    System.err.printf("%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS [ERROR] %2$s%n", now, msg);
                    if (throwable != null) {
                        throwable.printStackTrace(System.err);
                    }
                    break;
                default:
                    throw new IllegalArgumentException("illegal level: " + level);
            }
        } else {
            // 通过日志系统输出日志。
            final Logger logger = LoggerFactory.getLogger(Application.class);
            switch (level) {
                case LOG_TRACE:
                    logger.trace(msg);
                    break;
                case LOG_DEBUG:
                    logger.debug(msg);
                    break;
                case LOG_INFO:
                    logger.info(msg);
                    break;
                case LOG_WARN:
                    logger.warn(msg, throwable);
                    break;
                case LOG_ERROR:
                    logger.error(msg, throwable);
                    break;
                default:
                    throw new IllegalArgumentException("illegal level: " + level);
            }
        }
    }
}
